import EPub from "epub";
import NSJsDom from "jsdom";
import {
  nodeFileSystem,
  nodePath,
  PROJECT_CACHE,
  PROJECT_STATIC,
  pathToUrl,
  readJson,
  isStartWithProtocol,
  posixJoinPath
} from "./path-helper";
import EPubGen from "epub-gen";

export const symbolKeys = {
  escapedChapterTitle: Symbol("escapedChapterTitle"),
} as const;

export async function loadEpub(epubPath: string): Promise<EPub> {
  return new Promise((resolve, reject) => {
    const epubInfo = new EPub(epubPath);
    epubInfo.on("end", () => {
      resolve(epubInfo);
    });
    epubInfo.on("error", (err) => {
      reject(err);
    });
    epubInfo.parse();
  });
}

export function getCacheRootOf(
  epubTitle: string,
  isCreateWhenNonExists = false
) {
  const targetPath = nodePath.join(PROJECT_CACHE, epubTitle);
  if (isCreateWhenNonExists) {
    if (!nodeFileSystem.existsSync(targetPath)) {
      nodeFileSystem.mkdirSync(targetPath, {
        recursive: true,
      });
    }
  }
  return targetPath;
}

export async function getChapterContent(
  epubInfo: EPub,
  chapterId: string
): Promise<string> {
  return new Promise((resolve, reject) => {
    epubInfo.getChapter(chapterId, (err, text) => {
      if (err) {
        reject(err);
      } else {
        resolve(text);
      }
    });
  });
}

export const COMPARE_TOC_KEY_FULL_LIST = [
  "level",
  "order",
  "id",
  "href",
  "title",
] as const;

export function isSameToc(
  tocA: EPub.TocElement,
  tocB: EPub.TocElement,
  keyList: Array<keyof EPub.TocElement> = COMPARE_TOC_KEY_FULL_LIST.slice()
) {
  return keyList.every((keyName) => {
    return tocA[keyName] === tocB[keyName];
  });
}

export function getTocIndexIn(
  tocList: Array<EPub.TocElement>,
  targetToc: EPub.TocElement,
  compareKeyList: Array<
    keyof EPub.TocElement
  > = COMPARE_TOC_KEY_FULL_LIST.slice()
) {
  return tocList.findIndex((tocOne) => {
    return isSameToc(tocOne, targetToc, compareKeyList);
  });
}

export async function cacheImg(
  epubInfo: EPub,
  imgSrc: string,
  imagesCachePath: string
) {
  const imgId = getImageId(epubInfo, imgSrc);
  const extName = nodePath.extname(imgSrc);
  const imgName = imgId + extName;
  const imgPath = nodePath.join(imagesCachePath, imgName);
  if (nodeFileSystem.existsSync(imgPath)) {
    return imgPath;
  }
  const { data } = await loadImageFromEPub(epubInfo, imgId);
  await nodeFileSystem.promises.writeFile(imgPath, data);
  return imgPath;
}

export function getImageRootFromEPub(epubInfo: EPub): string {
  const keyName = "imageroot";
  if (keyName in epubInfo) {
    return (epubInfo[keyName] as any);
  }
  return "";
}

export function getImageId(
  epubInfo: EPub,
  imgSrc: string
): string {
  const imgRoot = getImageRootFromEPub(epubInfo);
  if (imgSrc.startsWith(imgRoot)) {
    const partList = imgSrc.replace(imgRoot, "").split("/");
    if (partList[0]) {
      return partList[0];
    }
    return partList[1] || "";
  }
  return imgSrc;
}

export function loadImageFromEPub(
  epubInfo: EPub,
  imgId: string
): Promise<{data: Buffer<ArrayBufferLike>, mimeType: string}> {
  return new Promise((resolve, reject) => {
    epubInfo.getImage(imgId, (err, data, mimeType) => {
      if (err) {
        reject(err);
      } else {
        resolve({
          data,
          mimeType,
        });
      }
    });
  });
}

export async function toCacheChapterContent(
  epubInfo: EPub,
  originChapterContent: string,
  imagesCachePath: string,
): Promise<string> {
  const chapterDom = new NSJsDom.JSDOM(originChapterContent);
  const imgElmList = Array.from(chapterDom.window.document.body.querySelectorAll("img"));
  if (imgElmList.length) {
    const imgCacheList = imgElmList.map((imgElm) => {
      return cacheImg(epubInfo, imgElm.src, imagesCachePath).then((cacheSrc) => {
        imgElm.src = pathToUrl(cacheSrc).href;
      });
    });
    await Promise.all(imgCacheList);
  }
  return chapterDom.window.document.body.innerHTML;
}

export async function cacheChapter(
  epubInfo: EPub,
  chapterInfo: EPub.TocElement,
  cacheDirPath: string,
  imagesCachePath: string
) {
  const originChapterContent = await getChapterContent(epubInfo, chapterInfo.id);
  const chapterContent = await toCacheChapterContent(epubInfo, originChapterContent, imagesCachePath);
  const chapterTotalNum = epubInfo.toc.length;
  const indexLength = chapterTotalNum.toString().length;
  const chapterIndex = getTocIndexIn(epubInfo.toc, chapterInfo);
  if (chapterIndex < 0) {
    throw new Error(
      `cannot found [${chapterInfo.title}]#{${chapterInfo.id}}@[${chapterInfo.level}_${chapterInfo.order}] in epub.flow`
    );
  }
  const indexString = chapterIndex!.toString().padStart(indexLength, "0");
  const chapterExt = nodePath.extname(chapterInfo.href);
  const chapterHtmlName = indexString + chapterExt;
  const chapterHtmlPath = nodePath.join(cacheDirPath, chapterHtmlName);
  return nodeFileSystem.promises.writeFile(
    chapterHtmlPath,
    chapterContent,
    "utf-8"
  );
}

export interface ParseAdvanceOption {
  updateTocInfo: (
    tocInfo: EPub.TocElement
  ) => EPub.TocElement & { [symbolKeys.escapedChapterTitle]?: string };
}

export interface ParseToHtmlResult {
  status: "rejected" | "fulfilled";
  tocInfo: EPub.TocElement;
  tocIndex: number;
  reason: any;
}

export async function parseToHtml(
  epubInfo: EPub,
  articlesCachePath: string,
  imagesCachePath: string,
): Promise<Array<ParseToHtmlResult>> {
  const cacheList = epubInfo.toc.map((tocInfo) => {
    return cacheChapter(epubInfo, tocInfo, articlesCachePath, imagesCachePath);
  });
  const cacheResultList = await Promise.allSettled(cacheList);
  const resultList = cacheResultList.map((cacheResult, cacheIndex) => {
    const tocInfo = epubInfo.toc.at(cacheIndex)!;
    return {
      status: cacheResult.status,
      tocInfo,
      tocIndex: cacheIndex,
      flowIndex: getTocIndexIn(epubInfo.flow, tocInfo),
      reason: cacheResult.status === "rejected" ? cacheResult.reason : "",
    };
  });
  return resultList;
}

export async function commonHandleParseToHtmlResult(
  epubInfo: EPub,
  resultList: Array<ParseToHtmlResult>,
  logPath: string
) {
  const failList = resultList.filter(
    (resultOne) => resultOne.status === "rejected"
  );
  if (failList.length > 0) {
    console.log(`cache chapter fail:`);
    console.log(
      failList.map((failOne) => {
        return `${failOne.tocIndex}:[${failOne.tocInfo.title}]`;
      })
    );
    const cacheLog = failList
      .map((failOne) => {
        const partList: string[] = [
          `${failOne.tocIndex}:[${failOne.tocInfo.title}]`,
          failOne.reason,
        ];
        return partList.join("\n");
      })
      .join("-----\n-----");
    await nodeFileSystem.promises.writeFile(logPath, cacheLog, "utf8");
  } else {
    console.log("cache chapter success");
  }
}

export function getCacheBookPath(bookTitle: string, dirType: "raw" = "raw") {
  return nodePath.join(PROJECT_CACHE, dirType, bookTitle);
}

export function getArticlesCachePath(
  bookRootPath: string,
  isMkDirWhenNonExist = false
) {
  return getCachePath(bookRootPath, "articles", isMkDirWhenNonExist);
}

export function getImagesCachePath(
  bookRootPath: string,
  isMkDirWhenNonExist = false
) {
  return getCachePath(bookRootPath, "images", isMkDirWhenNonExist);
}

export function getCachePath(
  bookRootPath: string,
  cacheType: string,
  isMkDirWhenNonExist = false
) {
const cachePath = nodePath.join(bookRootPath, cacheType);
  if (isMkDirWhenNonExist && !nodeFileSystem.existsSync(cachePath)) {
    nodeFileSystem.mkdirSync(cachePath, {
      recursive: true,
    });
  }
  return cachePath;
}

export async function getEPubInfoByFileName(
  targetFileName: string
) {  
  const targetFilePath = posixJoinPath(PROJECT_STATIC, targetFileName);
  const epubInfo = await loadEpub(targetFilePath);
  return epubInfo;
}

export async function writeEPubInfo(dirPath: string, epubInfo: EPub) {
  const jsonPath = nodePath.join(dirPath, "epub-info.json");
  if ("cover" in epubInfo.metadata) {
    const coverImageId = (epubInfo.metadata.cover || "") as string;
    if (!isStartWithProtocol(coverImageId)) {
      const { data, mimeType } = await loadImageFromEPub(epubInfo, coverImageId);
      const [_, suffix] = mimeType.split("/");      
      const coverImageName = nodePath.basename("cover-image." + suffix);
      const coverImagePath = nodePath.join(dirPath, coverImageName);
      await nodeFileSystem.promises.writeFile(coverImagePath, data);
      epubInfo.metadata.cover = coverImagePath;
    }    
  }
  return nodeFileSystem.promises.writeFile(jsonPath, JSON.stringify(epubInfo), "utf8");
}

export async function loadThenParseToHtml(
  targetFileName: string,
  bookCacheName: string
) {
  const epubInfo = await getEPubInfoByFileName(targetFileName);
  const bookCachePath = getCacheBookPath(bookCacheName);
  const articlesCachePath = getArticlesCachePath(bookCachePath, true);
  const imagesCachePath = getImagesCachePath(bookCachePath, true);
  await writeEPubInfo(bookCachePath, epubInfo);
  const resultList = await parseToHtml(epubInfo, articlesCachePath, imagesCachePath);
  return {
    epubInfo,
    resultList,
    articlesCachePath,
    bookCachePath,
  };
}

export async function getArticleContentListFromCachePath(
  articlesCachePath: string
) {
  const articleList = await nodeFileSystem.promises.readdir(articlesCachePath);
  const readPromiseList = articleList.map((articleOne) => {
    let articlePath = nodePath.join(articlesCachePath, articleOne);
    const articlePath1 = nodePath.join(articlesCachePath + "-1", articleOne);
    if (nodeFileSystem.existsSync(articlePath1)) {
      articlePath = articlePath1;
    }
    return nodeFileSystem.promises.readFile(articlePath, "utf8").then((data) => {
      return {
        data,
        name: articleOne,
      };
    });
  });
  return Promise.all(readPromiseList);
}


function getArticleTitle(tocList: Array<{title: string}>, articleIndex: number) {
  const targetToc = tocList[articleIndex];
  if (targetToc) {
    return targetToc.title;
  }
  return articleIndex.toString();
}

function getVersionNumber(versionStr: string) {
  return parseInt(versionStr) ?? 3;
}

export async function generateEPub(  
  bookCacheName: string,
  metaInfo: Partial<Omit<EPubGen.Options, "content">>
) {
  const epubPath = nodePath.join(PROJECT_CACHE, bookCacheName + ".epub");
  const bookCachePath = getCacheBookPath(bookCacheName);
  const articlesCachePath = getArticlesCachePath(bookCachePath);
  const epubInfoJson = nodePath.join(bookCachePath, "epub-info.json");
  let epubInfo: Record<string, any> = {};
  if (nodeFileSystem.existsSync(epubInfoJson)) {
    epubInfo = await readJson<Record<string, any>>(epubInfoJson);
  }
  const articleContentList = await getArticleContentListFromCachePath(articlesCachePath);
  articleContentList.sort((a, b) => {
    if (a.name < b.name) {
      return -1;
    }
    return 1;
  });
  const contentList = articleContentList.map((articleOne, articleIndex) => {
    return {
      title: getArticleTitle(epubInfo.toc || [], articleIndex),
      data: articleOne.data,
    };
  });
  const outputPath = metaInfo.output || epubPath;
  const epubGen = new EPubGen({
    ...epubInfo.metadata,
    ...metaInfo,
    version: metaInfo.version || getVersionNumber(epubInfo.version || ""),
    title: metaInfo.title || bookCacheName,
    output: outputPath,
    content: contentList,
  });
  await epubGen.promise.then(() => {
    console.log("outputPath:", outputPath);
  }, (err) => {
    console.error(err);
  });
}
